/* 目的： 基于 UDP 多播协议的 客户端程序，主要功能是接收频道列表，用户选择一个频道后，通过 管道 将数据传送给子进程（mplayer）
*         以播放频道内容。
*	1、创建管道 pfd ，然后使用 fork() 创建子进程。
*	2、子进程通过 管道 读取数据并执行 mplayer 播放接收到的频道内容。
*	3、父进程 创建 UDP 套接字，加入多播组，接收频道列表，并展示给用户，用户选择一个频道。
*	4、父进程将用户选择的频道数据通过 管道 传递给 子进程播放。
*	5、程序结束时，释放资源，关闭 套接字 和 管道。
*/

#include <stdio.h>
#include <errno.h>
#include <signal.h>
#include <unistd.h>    
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <string.h>
#include <stdlib.h>
#include <sys/socket.h>  //网络相关的库
#include <netinet/in.h>  
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <net/if.h>

#include "netradio_proto.h"    //自定义头文件，这个头文件包含与频道相关的结构体和常量等

#define BUFSIZE	4096  //用来表示缓冲区的大小，值为4096字节

//信号处理函数
//定义了一个空的信号处理函数（实际上并没有任何功能），这个函数将在SIGINT信号发生时被调用
//（通常是用户按下Ctrl+C终止程序）
static void sig_handler(int s)
{

}
//文件复制函数（未使用）
//定义了一个文件复制的函数，但在当前代码中并没有使用到。这个函数可能是预留的功能，或在其他地方需要实现
static int cp_file(int rfd, int wfd);


//主函数
int main(int argc, char *argv[])
{
	int fd;
	int pfd[2] = {};   //管道和子进程创建
	pid_t pid;
	int udp_socket;
	struct chn_list_st *rcv_list;	//整个频道列表结构
	struct list_entry *entry;	//单个频道的描述结构
	struct chn_data_st *rcv_data;	//频道数据
	union chn_recv_st *rcv_msg;
	struct sockaddr_in myaddr; // man 7 ip
	struct ip_mreqn imr;
	int len, choose;

	// sigaction
	signal(SIGINT, sig_handler);

//管道创建：
	//通过 pipe(pfd)创建一个管道。
	//pfd[0]用于读取管道数据（从父进程发送到子进程的输入）
	//pfd[1]用于写入管道数据（从子进程接收数据）
	//返回值：返回 0 表示成功，返回 -1 表示失败
	//功能：
	//pipe()创建一个管道， pfd 是一个数组，包含了两个文件描述符
	if (-1 == pipe(pfd)) {
		perror("pipe()");
		exit(1);
	}
//进程创建：
	//通过 fork()创建一个子进程。父进程会继续执行，而子进程会跳转到 if(0 == pid)语句块。
	//功能：
	//fork()创建一个子进程，父进程会继续执行，子进程会在 if(0 == pid) 块中执行
	//fork()没有参数  子进程返回 0； 父进程返回子进程的 PID（进程ID）； 出错返回 -1 
	//返回值： 父进程继续执行（pid > 0）;子进程执行 if(0 == pid)代码块
	pid = fork();
	if (-1 == pid) {
		perror("fork()");
		goto ERROR;
	}
//子进程：
	//关闭管道的写端（pfd[1]),然后通过 pause()等待信号。dup2(pfd[0]),0) 将管道的读端重定向
	//到标准输入，使得后续的mplayer可以通过便准输入接收数据。然后，子进程执行 mplayer，该命令会接收从管道读取的数据流
	//并进行播放。
//父进程：父进程继续执行，负责后续的 UDP 网络编程和信号处理。
	if (0 == pid) {
		//子进程不需要写管道，因此关闭 pfd[1]（写端）。 
		//读管道并执行命令
		close(pfd[1]);

		//pause()：子进程进入暂停状态，等待信号（如 SIGINT）来激活其处理。此时，子进程不会做任何事情，直到收到信号。
		pause(); // 等待信号到来
		
		//dup2(pfd[0], 0)：将管道的读端 (pfd[0]) 重定向到标准输入（文件描述符 0）。
		//这使得子进程的标准输入会从管道中读取数据，mplayer 会从管道获取播放的数据。
		dup2(pfd[0], 0);
		
		// 用文本文件调试
	/*	char buf[BUFSIZE] = {};
		int cnt;
		while (1) {
			cnt = read(0, buf, BUFSIZE);	
			if (-1 == cnt) {
				perror("read()");
				exit(1);
			} else if (0 == cnt) {
				exit(0);
			}
			write(1, buf, cnt);
		}*/

		//execl("/usr/bin/mplayer", "mplayer", "-", NULL);
		//通过 execlp() 启动 mplayer 播放器，并通过标准输入 - 从管道接收数据。
		//"-cache", "8192" 是 mplayer 的缓存设置，- 表示从标准输入读取数据。
		//execlp 用于执行一个程序，并替换当前进程的映像。"mplayer" 是要执行的程序名，后续参数是 mplayer 的命令行参数。
		execlp("mplayer", "mplayer", "-cache", "8192", "-", NULL);
		perror("execl()");
		exit(1);
	}

	
	close(pfd[0]);
	// 初始化套接字（创建 UDP 套接字）
	udp_socket = socket(AF_INET, SOCK_DGRAM, 0);
	if (-1 == udp_socket) {
		perror("socket()");
		exit(1);
	}

//绑定套接字：
	// 被动端需要bind地址
	myaddr.sin_family = AF_INET;
	myaddr.sin_port = htons(RCV_PORT);
	//绑定本地地址 0.0.0.0 和端口 RCV_PORT（需要定义这个端口号），这意味着该套接字将监听来自任何 IP 的 UDP 数据
	inet_aton("0.0.0.0"/*INADDR_ANY*/, &myaddr.sin_addr);
	//参数1：UDP套接字   参数2：包含本地地址和端口的struct sockaddr_in结构体 
	//参数3：bind()函数需要知道地址结构的大小
	if (-1 == bind(udp_socket, (struct sockaddr *)&myaddr, sizeof(myaddr))) {
		perror("bind()");
		goto ERROR;
	}

// 加入多播组
	//GROUP_ADDR 是多播地址
	//将多播组地址转换为 in_addr结构体，存储到 imr.imr_multiaddr 中
	inet_aton(GROUP_ADDR, &imr.imr_multiaddr);
	// 设置本地接口地址，通常为 0.0.0.0表示所有网络接口
	inet_aton("0.0.0.0", &imr.imr_address);
	// 虚拟网卡的索引值，索引名转换为值if_nametoindex(3)

	//ens33 是网络接口的名称（需要根据实际情况配置）。
	//这个配置 使得程序 能够接收该 多播组中的数据
	//获取网卡接口“ens33”的索引，用于指定多播发送的网络接口
	imr.imr_ifindex = if_nametoindex("ens33");

	//通过 setsockopt() 使得程序加入指定的多播组。
	//参数1：UDP的套接字   参数2：表示协议层为IP  
	//参数3：表示加入多播组  参数4：多播选项结构体，包含多播组地址、接口地址和接口索引
	//参数5：结构体大小
	if (-1 == setsockopt(udp_socket, IPPROTO_IP, IP_ADD_MEMBERSHIP, &imr, sizeof(imr))) {
		perror("setsockopt()");
		goto ERROR;
	}

//接收频道列表
	rcv_msg = malloc(MSG_SIZE);
	memset(rcv_msg, '\0', sizeof(rcv_msg));
	if (NULL == rcv_msg)
		goto ERROR;
	while (1) {
		//接收数据：
		//程序通过 recvfrom() 接收来自多播组的数据。数据被存放在 rcv_msg 中。
		len = recvfrom(udp_socket, rcv_msg, MSG_SIZE, 0, NULL, NULL);
		if (-1 == len) {
			if (errno == EINTR)
				continue;
			perror("recvfrom()");
			goto ERROR;
		}
		if (0 == len)
		{
			printf("-----\n");
			break;
		}
		// 如果是列表
		//如果接收到的数据频道 ID （rcv_msg->chnid） 等于  CHN_LIST_ID(可能是一个标识符，表示频道列表)
		if (rcv_msg->chnid == CHN_LIST_ID)
		{
			printf("*****\n");
			//跳出循环
			break;
		}
	}
	printf("%d\n", len);

//打印频道列表
	// 接收到了列表--->打印列表
	rcv_list = (void *)rcv_msg;
	int list_id = 1;

	//打印频道：
	//根据接收到的频道列表，遍历其中的每个频道信息，并输出每个频道的 chnid(频道ID) 和 descr(描述)
	//每个频道的长度通过 entry->len 获取
	for (entry = (void *)((char *)rcv_list + sizeof(rcv_list->chnid)); (char *)entry - (char *)rcv_list < len; entry = (void *)((char *)entry + entry->len)) {
		
		//printf("%d %s\n", entry->chnid, entry->descr);
		//printf("%d %s\n", list_id, entry->descr);
		//list_id++;
		//DEBUG
		printf("列表的id为 ：%d\n", entry->chnid);
		printf("描述：%s", entry->descr);

	/*	if(entry->chnid >= 3 || entry->chnid < 1)
		{
			fflush(NULL);
			printf("没有这玩意\n");
			break;
		}
	*/
		//if()
		sleep(1);
	}
//用户选择频道
	//提示用户输入频道ID（choose）,用户选择要播放的频道
	printf("请选择频道:");
	scanf("%d", &choose);


//向子进程传送数据
	kill(pid, SIGINT);

	// 接受频道数据
	rcv_data = (void *)rcv_msg;
	while (1) {
		memset(rcv_data, '\0', MSG_SIZE);

		//接收频道数据：
		//程序继续接收频道数据，如果接收到的频道 ID 与用户选择的频道匹配，则将频道数据通过 管道 传送给 子进程 进行播放
		len = recvfrom(udp_socket, rcv_data, MSG_SIZE, 0, NULL, NULL);
		printf("%d*****%ld  %d\n", rcv_data->chnid, strlen(rcv_data->msg), len);
	/*	if (-1 == len) {
			if (errno == EINTR)
				continue;
			perror("recvfrom()");
			goto ERROR;
		}
		if (0 == len)
			break;*/
		// 是否是选择的频道号
		if (rcv_data->chnid == choose)
			write(pfd[1], rcv_data->msg, len - sizeof(rcv_data->chnid));
	}
	printf("您所选择的频道已经关闭，拜拜!!!\n");

//资源释放与程序退出
	//资源释放： 释放动态分配的内存（rcv_msg），关闭套接字 和 管道，等待子进程结束，最后退出程序
	free(rcv_msg);
	rcv_msg = NULL;
	close(udp_socket);
	close(pfd[1]);

	wait(NULL);

	exit(0);

//错误处理：
	//在程序中的错误位置（如 pipe() , fork() , socket() 等函数调用失败时），会进入 ERROR标签
	//清理资源并退出程序
ERROR:
	close(pfd[0]);
	close(pfd[1]);

	exit(1);

